/*-
 * CompressionProcessor.java
 *
 * $LastChangedBy: stein $
 * $LastChangedRevision: 95 $
 * $LastChangedDate: 2007-02-13 16:19:24 +0000 (Tue, 13 Feb 2007) $
 * $HeadURL: svn://localhost/SMS/src/com/googlecode/attention/sms/comp/CompressionProcessor.java $
 *
 * Copyright (c) 2007, Stein Gunnar Bakkeby
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Stein Gunnar Bakkeby nor the names of its
 *       contributors may be used to endorse or promote products derived
 *       from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.googlecode.attention.sms.comp;

import static com.googlecode.attention.util.Convert.*;
import static com.googlecode.attention.util.SMSCharacterSet.*;

import java.io.IOException;

import com.googlecode.attention.util.IntArrayBuilder;


/** * Compression Processor, compresses and decompresses input data using * multiple compression algorithms. *  * @author Stein Gunnar Bakkeby */
public class CompressionProcessor {
  
  /* Field values */
  private static boolean isUCS2Enabled;
  private int currentUCS2Row = 0;
  private int overrideCharset = -1;

  private CH header;
  private KeywordProcessor kp = null;
  private CharacterGroupProcessor cgp = null;
  private PunctuationProcessor pp = null;
  private HuffmanProcessor hp = null;
  
  /* Static field values */
  /** Character Set Converter */
  public static final int CHARACTER_SET_CONVERTER   = 0;
  /** Punctuation Processor */
  public static final int PUNCTUATION_PROCESSOR     = 1;
  /** Keyword Processor */
  public static final int KEYWORD_PROCESSOR         = 2;
  /** UCS2 Processor */
  public static final int UCS2_PROCESSOR            = 3;
  /** Character Group Processor */
  public static final int CHARACTER_GROUP_PROCESSOR = 4;
  /** Huffman Processor */
  public static final int HUFFMAN_PROCESSOR         = 5;
  
  /**
   * Control Symbol: New 7 bit character.
   * <BR>
   * <BR>On encoding, if a character (octet) from the input stream in the
   * range 0 to 127 does not exist in the Huffman tree, then the New 7 bit
   * character symbol is Huffman encoded to the CD and bits 6 to 0 of the
   * original octet are copied unchanged to the CD. The Huffman tree would
   * then be updated to include the new character.
   * <BR>
   * <BR>On decoding the New 7 bit character symbol, the symbol itself is
   * discarded and the next 7 bits of the CD are copied unchanged to bits
   * 6-0 of the octet to be output, bit 7 of which is zero. The Huffman
   * tree would then be updated to include the new character.
   */
  public static final int CTRL_SYMB_NEW_7BIT_CHAR  = 256;
  /**
   * Control Symbol: New 8 bit character.
   * <BR>
   * <BR>The operation of this is identical to that of the New 7 bit
   * character except that on encoding, the input character is in the
   * range 128-255 and on decoding, bit 7 of the output character is
   * set to 1.
   */
  public static final int CTRL_SYMB_NEW_8BIT_CHAR  = 257;
  /** 
   * Control Symbol: Keyword.
   * <BR>
   * <BR>This symbol (Huffman encoded) prefixes a sequence of bits of
   * variable length in the CD that define a representation of characters
   * in the uncompressed stream by an entry in a keyword dictionary.
   * <BR>
   * <BR>On encoding, if a sequence of characters in the input stream can
   * be represented by an entry in a keyword dictionary, the Keyword symbol
   * is Huffman encoded to the CD followed by the bit sequence describing
   * the keyword entry. On decoding the Keyword symbol, the symbol itself
   * is discarded and the bit sequence describing the keyword entry is
   * passed to the Keyword processor to recovery the original character
   * sequence to be placed in the output stream.
   */
  public static final int CTRL_SYMB_KEYWORD        = 258;
  /**
   * Control Symbols: Character Group Transitions.
   * <BR>
   * <BR>These symbols signal transitions between groups of characters
   * defined within the Character Group processor. For example, if 2 groups
   * are defined to be the lower case and upper case characters then the
   * input stream:
   * <BR>&nbsp;&nbsp;&nbsp;&nbsp;"abcdefABCDEF" would become
   * "abcdef<Change Group>abcdef"
   * <BR>
   * <BR>On encoding, Character Group Transition symbols are generated by
   * the Character Group processor and simply passed to the Huffman
   * processor for encoding.
   * <BR>
   * <BR>On decoding a Character Group Transition symbol, it is simply
   * passed from the Huffman processor to the Character Group processor
   * which takes the appropriate action based its current state and the
   * group transition indicated.
   */
  public static final int CTRL_SYMB_CG_TRANS_0     = 259;
  /** Control Symbols: Character Group Transitions. */
  public static final int CTRL_SYMB_CG_TRANS_1     = 260;
  /** Control Symbols: Character Group Transitions. */
  public static final int CTRL_SYMB_CG_TRANS_2     = 261;
  /** Control Symbols: Character Group Transitions. */
  public static final int CTRL_SYMB_CG_TRANS_3     = 262;
  /** Control Symbols: Character Group Transitions. */
  public static final int CTRL_SYMB_CG_TRANS_4     = 263;
  /** Control Symbols: Character Group Transitions. */
  public static final int CTRL_SYMB_CG_TRANS_5     = 264;
  /** Control Symbols: Character Group Transitions. */
  public static final int CTRL_SYMB_CG_TRANS_6     = 265;
  /**
   * Control Symbols: New UCS2 Row.
   * <BR>
   * <BR>On encoding, if the next UCS2 character in the input stream has
   * a "row octet" of a different value to that of the previous character
   * in the input stream, the New UCS2 Row symbol is Huffman encoded to the
   * CD and the 8 bit of the new row octet are copied unchanged to the CD.
   * The new row octet is stored by the UCS2 processor as the "current
   * row octet" and subsequent input characters within the current row are
   * Huffman encoded as the 8 bit value of the character within the
   * "current row".
   * <BR>
   * <BR>On decoding the New UCS2 Row symbol, the symbol is discarded and
   * the next 8 bits are read from the CD and stored by the UCS2 processor
   * as the "current row octet". Subsequent UCS2 characters are decoded by
   * treating the 8 bit character values decoded by the Huffman processor
   * as characters within the "current row".
   */
  public static final int CTRL_SYMB_NEW_UCS2_ROW   = 266;
  /* End of static field values */
  
  /**
   * Constructor, creates the compression processor.
   */
  public CompressionProcessor() {
    header = new CH();
    isUCS2Enabled = true;
  }

  /**
   * Use this to indicate whether UCS2 encoding is enabled or not.
   * 
   * @param value True if enabled, false otherwise
   */
  public static void setUCS2Enabled(final boolean value) {
    isUCS2Enabled = value;
  }

  /**
   * This indicates whether UCS2 encoding is enabled or not.
   * 
   * @return True if UCS2 compression is enabled, false otherwise
   */
  public static boolean isUCS2Enabled() {
    return isUCS2Enabled;
  }

  
  /**
   * This will override the default charset as specified by
   * the CLC.
   * 
   * @param value True if original charset is to be overridden, false otherwise
   * @param charset The charset to override with,
   *                see Convert static field values
   */
  public void overrideCharset(final boolean value, final int charset) {
    overrideCharset = (value ? charset : -1);
  }
  
  /**
   * This will return the compression header.
   * 
   * @return The compression header
   */
  public CH getCH() {
    return header;
  }
  
  /**
   * This will set the compression header.
   * 
   * @param ch The header
   */
  public void setCH(final CH ch) {
    this.header = ch;
  }
  
  /**
   * This will compress a stream of characters based
   * on the compression header.
   * 
   * @param data The data to compress
   * @see com.googlecode.attention.util.Convert
   * @throws IOException If initialisation file cannot be read
   * @return The completed compressed data stream
   */
  public IntArrayBuilder compress(final int[] data) throws IOException {
    overrideCharset = -1;
    return compress(0, data);
  }
  
  /**
   * This will compress a stream of characters based
   * on the compression header.
   * 
   * @param charset The charset to use: See Convert static field values
   * @param data The data to compress
   * @see com.googlecode.attention.util.Convert
   * @throws IOException If initialisation file cannot be read
   * @return The completed compressed data stream
   */
  public IntArrayBuilder compress(final int charset, int[] data)
  throws IOException {
    //String comp = data;
    final IntArrayBuilder cd = new IntArrayBuilder(data.length); // ensure at least that length
    
    /* Step 1: Construct Compression Header (already made) */
    
    /* Step 2 */
    if (header.isPunctuationEnabled()) {
      if (pp == null) pp = new PunctuationProcessor(header); 
      else pp.changeInitialisation(header);  
    }
    if (header.isKeywordEnabled()) {
      if (kp == null) kp = new KeywordProcessor(header);
      else kp.changeInitialisation(header);
    }
    if (header.isCharacterGroupEnabled()) {
      if (cgp == null) cgp = new CharacterGroupProcessor(header);
      else cgp.changeInitialisation(header);
    }
    if (isUCS2Enabled) {
      if (header.getExtensionValue(CH.CHANGE_UCS2_ROW) != -1) {
        currentUCS2Row = header.getExtensionValue(CH.CHANGE_UCS2_ROW);
      }
    }
    if (hp == null) hp = new HuffmanProcessor(header);
    else hp.changeInitialisation(header);
    
    /* Step 3 */
    if (overrideCharset != -1 && overrideCharset != charset) {
      data = changeCharacterSet(data, overrideCharset, charset);
    }
    
    /* Step 4 */
    if (header.isPunctuationEnabled()) {
      data = pp.compress(data);
    }
    
    /* Step 5 */
    int length = data.length;
    
    /* Step 6 */
    boolean keywordEnabled   = header.isKeywordEnabled();
    boolean chargroupEnabled = header.isCharacterGroupEnabled();
    for (int c = 0; c < length;) { // c == current char
      int step = 7;
      if (keywordEnabled) {
        KeywordMatch km = kp.compress(data, c);
        if (km.isMatchFound()) {
          if (chargroupEnabled) {
            int[] s = cgp.compress(CTRL_SYMB_KEYWORD, false).toArray();
            for (int k = 0; k < s.length; k++) {
              cd.append(hp.compress(s[k]));
            }
          }
          c = km.getCurrentIndex();
          cd.append(hp.compress(CTRL_SYMB_KEYWORD));
          cd.append(km.getBitstream());
          step = 10;
        }
      }
      
      /* Step 7 */
      if (step == 7) {
        if (charset == CHARSET_UCS2) {
          int row = data[c] >> 8;
          if (row != currentUCS2Row) {
            cd.append(hp.compress(CTRL_SYMB_NEW_UCS2_ROW));
            cd.append(row);
          }
          data[c] &= 0xFF; // (B7|B6|B5|B4|B3|B2|B1|B0)
        }
        /* Step 8 */
        if (chargroupEnabled) {
          int[] s = cgp.compress(data[c], c + 1 >= length).toArray();
          for (int k = 0; k < s.length; k++) {
            cd.append(hp.compress(s[k]));
          }
          c++;
        } else {
          /* Step 9 */
          cd.append(hp.compress(data[c]));
          c++;
        }
      }
      /* Step 10: Increment current character (implemented above) */
    }
    
    /* Step 11 */
    final IntArrayBuilder output = new IntArrayBuilder();
    
    output.append(header.toArray());
    length = cd.length();
    int p;
    for (p = 0; p + 8 < length; p += 8) {
      output.append(binToInt(cd.substring(p, p + 8)));
    }
    final int rest = length - p;
    final int pad  = 8 - rest;
    if (rest == 0) {
      output.append(new int[]{0, 0});
      
    } else if (rest > 0 && rest < 6) {
      final IntArrayBuilder bin = new IntArrayBuilder();
      bin.append(cd.substring(p));
      bin.append(intToBin(rest, pad));
      output.append(binToInt(bin));
    } else if (rest > 5) {
      final IntArrayBuilder bin = new IntArrayBuilder();
      bin.append(cd.substring(p));
      bin.append(intToBin(rest, pad + 8));
      output.append(binToInt(bin));
    }
    
    return output;
  }
  
  /**
   * This will decompress a compressed data stream.
   * 
   * @param comp The data to be decompressed
   * @throws IOException If initialisation file cannot be read
   * @return The decompressed stream
   */
  public IntArrayBuilder decompress(final IntArrayBuilder comp)
  throws IOException {
    overrideCharset = -1;
    return decompress(0, comp);
  }
  
  /**
   * This will decompress a compressed data stream.
   * 
   * @param charset The charset to use: See Convert static field values
   * @param comp The data to be decompressed
   * @throws IOException If initialisation file cannot be read
   * @return The decompressed stream
   */
  public IntArrayBuilder decompress(final int charset,
                                    IntArrayBuilder comp)
                                    throws IOException {
    IntArrayBuilder decomp = new IntArrayBuilder();
    /* Step 1: Interpret the compression header */
    final IntArrayBuilder header = new IntArrayBuilder();
    boolean run = true;
    for (int i = 0; run; i++) {
      //int[] tmp = comp.substring(i, i + 2).toArray();
      int tmp = comp.intAt(i);
      if ((tmp & 0x80) == 0) {
        run = false;
        comp = comp.substring(i + 1);        
      }
      header.append(tmp);
    }
    final CH ch = new CH(header.toArray());
    
    /* Step 2: Initialise algorithms */
    final boolean punctuationEnabled = ch.isPunctuationEnabled(); 
    if (punctuationEnabled) {
      if (pp == null) pp = new PunctuationProcessor(ch); 
      else pp.changeInitialisation(ch);  
    }
    boolean keywordEnabled = ch.isKeywordEnabled();
    if (keywordEnabled) {
      if (kp == null) kp = new KeywordProcessor(ch);
      else kp.changeInitialisation(ch);
    }
    boolean charGroupEnabled = ch.isCharacterGroupEnabled(); 
    if (charGroupEnabled) {
      if (cgp == null) cgp = new CharacterGroupProcessor(ch);
      else cgp.changeInitialisation(ch);
    }
    if (isUCS2Enabled) {
      if (ch.getExtensionValue(CH.CHANGE_UCS2_ROW) != -1) {
        currentUCS2Row = ch.getExtensionValue(CH.CHANGE_UCS2_ROW);
      }
    }
    if (hp == null) hp = new HuffmanProcessor(ch);
    else hp.changeInitialisation(ch);
    
    /* Step 3: Interpret the compression footer */
    IntArrayBuilder bin = new IntArrayBuilder();
    int length = comp.length();
    for (int i = 0; i < length; i++) {
      bin.append(intToBin(comp.intAt(i), 4));
    }
    int numberOfBits = bin.length();
    IntArrayBuilder footer = bin.substring(numberOfBits - 3);
    int rest = binToInt(footer);
    int pad = 8 - rest;
    if (rest >= 0 && rest < 6) {
      bin = bin.substring(0, numberOfBits - pad);
    } else if (rest > 5) {
      bin = bin.substring(0, numberOfBits - (pad + 8));
    }
    int bitIndex = 0;
    
    numberOfBits = bin.length();
    /* Step 4 */
    for (; bitIndex < numberOfBits;) {
      int step = 6;
      int currentSymbol = hp.decompress(bin.substring(bitIndex));
      bitIndex += hp.getNumberOfProcessedBits();
      
      /* Step 5 */
      if (keywordEnabled) {
        if (currentSymbol == CTRL_SYMB_KEYWORD) {
          IntArrayBuilder keybits = bin.substring(bitIndex);
          decomp.append(kp.decompress(keybits));
          bitIndex += kp.getNumberOfProcessedBits();
          step = 9;
        }
      }
      if (step == 6) {
        /* Step 6 */
        if (charGroupEnabled) {
          switch (currentSymbol) {
          case CTRL_SYMB_CG_TRANS_0:
          case CTRL_SYMB_CG_TRANS_1:
          case CTRL_SYMB_CG_TRANS_2:
          case CTRL_SYMB_CG_TRANS_3:
          case CTRL_SYMB_CG_TRANS_4:
          case CTRL_SYMB_CG_TRANS_5:
          case CTRL_SYMB_CG_TRANS_6:
            cgp.decompress(currentSymbol);
            step = 9;
            break;
            
          default:
            if (currentSymbol <= 255) { // i.e. not a control symbol
              currentSymbol = cgp.decompress(currentSymbol).intAt(0);
            }
            step = 7;
            break;
          }
        }
        if (step == 7) {
          /* Step 7 */
          if (overrideCharset == CHARSET_UCS2) {
            if (currentSymbol == CTRL_SYMB_NEW_UCS2_ROW) {
              int row = binToInt(bin.substring(bitIndex,bitIndex + 8));
              bitIndex += 8;
              currentUCS2Row = row;
              step = 9;
            } else {
              currentSymbol = binToInt(
                              intToBin(currentUCS2Row, 8)
                            + intToBin(currentSymbol,  8));
            }
          }
          /* Step 8 */
          if (step != 9) {
            decomp.append(currentSymbol);
          } 
        }
      }
      /* Step 9 */
    } // <-- goto Step 4 if not all bits have been processed
    
    /* Step 10 */
    if (punctuationEnabled) {
      decomp = pp.decompress(decomp);
    }
    
    /* Step 11 */
    if (overrideCharset != -1 && overrideCharset != charset) {
      decomp = changeCharacterSet(decomp, overrideCharset, charset);
    }
    
    return decomp;
  }
}